package compiler;

import java.util.Hashtable;

public class Lexer {
	
	/**
	 * This class will be used to tell the user the line where the error occourred
	 */
    class LexerContext {
        private int lineNumber;
        private String line;

        public LexerContext(int lineNumber, String line) {
            setLineNumber(lineNumber);
            setLine(line);
        }

        public String getLine() {
            return line;
        }

        public int getLineNumber() {
            return lineNumber;
        }

        public void setLine(String line) {
            this.line = line;
        }

        public void setLineNumber(int lineNumber) {
            this.lineNumber = lineNumber;
        }

    }

    private static final int MAX_INTEGER_VALUE = 32768;
    private static final Hashtable<String, Symbol> keywordsTable = new Hashtable<String, Symbol>();

    private char[] input;
    private Symbol token;
    private int tokenPos;
    private int lineNumber;
    private String stringValue;
    private int numericValue;

    static {
        keywordsTable.put(Symbol.INT.toString(), Symbol.INT);
        keywordsTable.put(Symbol.STR.toString(), Symbol.STR);
        keywordsTable.put(Symbol.PRINT.toString(), Symbol.PRINT);
        keywordsTable.put(Symbol.READ.toString(), Symbol.READ);
        keywordsTable.put(Symbol.IF.toString(), Symbol.IF);
        keywordsTable.put(Symbol.ELSE.toString(), Symbol.ELSE);
        keywordsTable.put(Symbol.WHILE.toString(), Symbol.WHILE);
        keywordsTable.put(Symbol.FUNC.toString(), Symbol.FUNC);
        keywordsTable.put(Symbol.AND.toString(), Symbol.AND);
        keywordsTable.put(Symbol.OR.toString(), Symbol.OR);
        keywordsTable.put(Symbol.RETURN.toString(), Symbol.RETURN);
    }

    public Lexer(char[] input) {
        this.input = input;
        if(this.input.length == 0) this.input = new char[1];
        this.input[this.input.length - 1] = '\0';
        tokenPos = 0;
        lineNumber = 1;
    }

    public LexerContext getCurrentContext() {
        return new LexerContext(lineNumber, "");
    }

    public int getNumericValue() {
        return numericValue;
    }

    public String getStringValue() {
        return stringValue;
    }

    public Symbol getToken() {
        return token;
    }

    /**
     * Gets the next token when the token is an identifier or a keyword
     */
    private void nextIdentifierOrKeyword() {
        StringBuffer sb = new StringBuffer();

        while (Character.isLetter(input[tokenPos])) {
            sb.append(input[tokenPos]);
            tokenPos++;
        }

        stringValue = sb.toString();
        token = keywordsTable.get(stringValue);

        if (token == null)
            token = Symbol.IDENT;

        if (Character.isDigit(input[tokenPos]))
            Utils.error("numbers are not allowed in indentifiers", this.getCurrentContext());
    }

    /**
     * Gets the next token when the token is a number
     */
    private void nextNumber() {
        StringBuffer sb = new StringBuffer();

        while (Character.isDigit(input[tokenPos])) {
            sb.append(input[tokenPos]);
            tokenPos++;
        }

        try {
            token = Symbol.NUMBER;
            numericValue = Integer.parseInt(sb.toString());
            if (numericValue > MAX_INTEGER_VALUE)
                throw new NumberFormatException();
        }
        catch (NumberFormatException e) {
            Utils.error("Not a valid integer", getCurrentContext());
        }
    }

    /**
     * Gets the next token when the token is a symbol or a string value
     */
    private void nextSymbolOrValue() {
        char initialChar = input[tokenPos];

        tokenPos++;
        switch (initialChar) {
            case '=':
                if (input[tokenPos] == '=') {
                    tokenPos++;
                    token = Symbol.EQ;
                }
                else
                    token = Symbol.ASSIGN;
                break;
            case '<':
                if (input[tokenPos] == '=') {
                    tokenPos++;
                    token = Symbol.LE;
                }
                else
                    token = Symbol.LT;
                break;
            case '>':
                if (input[tokenPos] == '=') {
                    tokenPos++;
                    token = Symbol.GE;
                }
                else
                    token = Symbol.GT;
                break;
            case '!':
                if (input[tokenPos] == '=') {
                    tokenPos++;
                    token = Symbol.NE;
                }
                else
                    Utils.error("= expected", getCurrentContext());
                break;
            case '-':
                if (input[tokenPos] == '>') {
                    tokenPos++;
                    token = Symbol.ARROW;
                }
                else
                    token = Symbol.MINUS;
                break;
            case '\'':
                StringBuffer sb = new StringBuffer();
                while (Character.isDigit(input[tokenPos])
                        || Character.isLetter(input[tokenPos])
                        || input[tokenPos] == ' ') {
                    sb.append(input[tokenPos]);
                    tokenPos++;
                }

                token = Symbol.STRINGVALUE;
                stringValue = sb.toString();

                if (input[tokenPos] != '\'') {
                    Utils.error("Invalid string: " + stringValue,
                            getCurrentContext());
                    this.nextToken();
                }
                tokenPos++;
                break;
            case '+':
                token = Symbol.PLUS;
                break;
            case '*':
                token = Symbol.MULT;
                break;
            case '/':
                token = Symbol.DIV;
                break;
            case '(':
                token = Symbol.LEFTPAR;
                break;
            case ')':
                token = Symbol.RIGHTPAR;
                break;
            case ';':
                token = Symbol.SEMICOLON;
                break;
            case ':':
                token = Symbol.COLON;
                break;
            case ',':
                token = Symbol.COMMA;
                break;
            case '.':
                token = Symbol.DOT;
                break;
            default:
                Utils.error("Unknown symbol: " + initialChar,
                        getCurrentContext());

        }
    }
    
    public void skipTerminal() {
    	if(token == Symbol.DOT || token == Symbol.COMMA || token == Symbol.LEFTPAR || token == Symbol.RIGHTPAR || token == Symbol.SEMICOLON)
    		nextToken();
    	if(token == Symbol.EOF)
    		Utils.error("Unexpected EOF", getCurrentContext());
    }
    
    public void skipToNextStatement(boolean skipSemicolon) {
    	while(token != Symbol.EOF && token != Symbol.SEMICOLON && token != Symbol.DOT) 
    		nextToken();
    	if((token == Symbol.SEMICOLON || token == Symbol.DOT) && skipSemicolon)
    		nextToken();
    }

    /**
     * Gets the next token
     */
    public void nextToken() {
        char initialChar = input[tokenPos];

        while (initialChar == ' ' || initialChar == '\r' || initialChar == '\t'
            || initialChar == '\n') {
            if (initialChar == '\n')
                lineNumber++;
            tokenPos++;
            initialChar = input[tokenPos];
        }
        
        if(input[tokenPos] == '/' && input[tokenPos + 1] == '*') {
        	removeComments();
        } else {

        	if (initialChar == '\0')
        		token = Symbol.EOF;
        	else if (Character.isLetter(initialChar))
        		nextIdentifierOrKeyword();
        	else if (Character.isDigit(initialChar))
        		nextNumber();
        	else
        		nextSymbolOrValue();
        }
    }

	private void removeComments() {
		tokenPos += 2;
		while(input[tokenPos] != '\0' && input[tokenPos] != '*' && input[tokenPos + 1] != '/') {
			if(input[tokenPos] == '\n') lineNumber++;
			tokenPos++;
		}
		if(input[tokenPos] != '\0') tokenPos += 2;
		else Utils.error("unterminated comment", getCurrentContext());
		nextToken();
	}

    public void setNumericValue(int numericValue) {
        this.numericValue = numericValue;
    }

    public void setStringValue(String stringValue) {
        this.stringValue = stringValue;
    }

    public void setToken(Symbol token) {
        this.token = token;
    }

}
